feat(delivery): relaycast target — agents reply over the relay (#254 pt.1)#255
Conversation
…254, part 1) Adds `relaycast` as a first-class delivery target alongside slack/telegram, so the existing "reply to origin transport" pattern (hn-monitor/inbox-buddy/joke-bot) covers agent-to-agent relay replies with no per-persona code. Unlike slack/telegram (config-driven via persona inputs), the relaycast reply is EVENT-driven: the `to` address is the inbound message's sender, supplied by the caller via `transports.relaycast = { to, sender? }`. `resolveDeliveryTargets` stays slack/telegram-only; relaycast is added in createDelivery when an address is present, so existing callers are unaffected. The default sender DMs the peer via `POST /v1/dm` with the box's injected RELAY_API_KEY. Base URL is a single source of truth (`DEFAULT_RELAYCAST_URL = https://cast.agentrelay.com`) overridable per-env via `RELAYCAST_URL` > `RELAY_BASE_URL` — easy to change as the gateway cutover settles. - types: `RelaycastRef`, `RelaycastSender`, `RelaycastTarget`, `DeliveryProvider`; widen `MessageRef`/`DeliveryClient.targets`/`onlyTargets` to include relaycast - new `relaycast.ts`: `DEFAULT_RELAYCAST_URL`, `resolveRelaycastUrl`, `defaultRelaycastSender` - tests: URL precedence + relaycast delivery path (6/6 green), typecheck clean Next (separate): runtime `ctx.relay.reply()` + surface inbound sender on the relay event; cloud default `inbox_selectors` to @self; then publish + consume in agents and drop hn-monitor's slack-only relay fallback. Refs #254 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 47 minutes and 37 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughRelaycast is added as a delivery target with new public types, URL resolution, and a default sender. Delivery creation now discovers relaycast, builds its transport config, and send dispatch records relaycast refs and failures. Tests cover URL selection, target filtering, and send behavior. ChangesRelaycast contracts and helper
Relaycast delivery target
Sequence Diagram(s)sequenceDiagram
participant createDelivery
participant DeliveryClientImpl
participant defaultRelaycastSender
participant RelaycastDMEndpoint
createDelivery->>DeliveryClientImpl: pass relaycast { to, sender }
DeliveryClientImpl->>defaultRelaycastSender: dm(to, text)
defaultRelaycastSender->>RelaycastDMEndpoint: POST /v1/dm with bearer auth
RelaycastDMEndpoint-->>defaultRelaycastSender: ok / messageId
defaultRelaycastSender-->>DeliveryClientImpl: RelaycastRef or { ok: false }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new event-driven relaycast delivery provider for agent-to-agent communication, alongside existing Slack and Telegram providers. It includes the implementation of the Relaycast sender, configuration resolution, type updates, and comprehensive unit tests. The feedback suggests utilizing the project's existing fetchWithTimeout helper instead of the global fetch when sending DMs to prevent potential hanging requests.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import type { WorkforceCtx } from '@agentworkforce/runtime'; | ||
| import type { RelaycastSender } from './types.js'; |
There was a problem hiding this comment.
Import 'fetchWithTimeout' from './helpers.js' to enable making the relaycast HTTP request with a timeout, preventing potential hanging.
| import type { WorkforceCtx } from '@agentworkforce/runtime'; | |
| import type { RelaycastSender } from './types.js'; | |
| import type { WorkforceCtx } from '@agentworkforce/runtime'; | |
| import type { RelaycastSender } from './types.js'; | |
| import { fetchWithTimeout } from './helpers.js'; |
| const res = await fetch(`${baseUrl}/v1/dm`, { | ||
| method: 'POST', | ||
| headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }, | ||
| body: JSON.stringify({ to, text }) | ||
| }); | ||
| if (!res.ok) { | ||
| ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: res.status }); | ||
| return { ok: false }; | ||
| } |
There was a problem hiding this comment.
Use 'fetchWithTimeout' instead of the global 'fetch' to prevent the request from hanging indefinitely if the relaycast gateway is unresponsive. This aligns with the project's existing pattern of using timeouts for external network requests.
| const res = await fetch(`${baseUrl}/v1/dm`, { | |
| method: 'POST', | |
| headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }, | |
| body: JSON.stringify({ to, text }) | |
| }); | |
| if (!res.ok) { | |
| ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: res.status }); | |
| return { ok: false }; | |
| } | |
| const res = await fetchWithTimeout(baseUrl + '/v1/dm', { | |
| method: 'POST', | |
| headers: { authorization: 'Bearer ' + apiKey, 'content-type': 'application/json' }, | |
| body: JSON.stringify({ to, text }) | |
| }); | |
| if (!res) { | |
| ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: 'timeout or network error' }); | |
| return { ok: false }; | |
| } | |
| if (!res.ok) { | |
| ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: res.status }); | |
| return { ok: false }; | |
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e8a6245bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * rather than crashing the handler. | ||
| */ | ||
| export function defaultRelaycastSender(ctx: WorkforceCtx): RelaycastSender { | ||
| const apiKey = process.env.RELAY_API_KEY?.trim(); |
There was a problem hiding this comment.
Use an agent token for relaycast DMs
This default sender captures RELAY_API_KEY as the bearer for POST /v1/dm, but the official Relaycast docs distinguish the workspace key (RELAY_API_KEY) from the agent token (RELAY_AGENT_TOKEN) and list DMs among commands that act as an agent; the OpenAPI /dm endpoint is secured with agentToken (README, OpenAPI). In real agent boxes this means the default relay reply is authenticated with the workspace key rather than the sender's agent identity, so replies over relaycast will be rejected unless tests inject a fake sender.
Useful? React with 👍 / 👎.
| const data = (await res.json().catch(() => null)) as | ||
| | { message?: { id?: unknown }; messageId?: unknown; id?: unknown } | ||
| | null; | ||
| const rawId = data?.message?.id ?? data?.messageId ?? data?.id; |
There was a problem hiding this comment.
Unwrap Relaycast success responses before reading the id
Relaycast REST responses are wrapped as { ok: true, data: ... }, and the /dm response's data contains the DM message/legacy id fields (OpenAPI). This parser only looks for message, messageId, or id at the top level, so a successful real DM returns a RelaycastRef with messageId: '', breaking callers that rely on the delivered message id.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/delivery/src/delivery.ts`:
- Around line 135-142: The relaycast branch in DeliveryClient.publish() still
performs a blocking sendRelaycast() call, which violates the non-blocking
contract defined by DeliveryClient.publish() in the types. Update the publish
flow around the sendRelaycast() path so relaycast is either routed through a
true non-blocking implementation or excluded from publish()/nonBlocking handling
until that exists, and make sure the behavior stays consistent with the existing
task/refs/errors pattern in delivery.ts.
- Around line 244-252: The sendRelaycast flow in delivery.ts is treating an ok
response with a missing messageId as a successful receipt by returning a
RelaycastRef with an empty id. Update sendRelaycast to require a real messageId
from rc.sender.dm: if res.ok is false or messageId is missing/empty, log the
warning and return null instead of constructing a ref. Keep the existing
sendRelaycast and RelaycastRef logic aligned with the Slack/Telegram delivery
behavior so only valid receipts are reported as success.
In `@packages/delivery/src/relaycast.ts`:
- Around line 37-42: The relaycast send/publish path currently uses an unbounded
fetch in the relaycast module, so it can hang indefinitely when the relay
gateway stalls. Update the relaycast request logic in the send/publish flow to
use the same timeout-and-fallback pattern used for bounded delivery writes in
delivery.ts, and ensure failures resolve to a graceful { ok: false } result
instead of blocking. Use the relaycast send/publish function and its fetch call
as the place to apply the timeout wrapper and fallback handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c6325808-6746-4830-80c3-c0e097ba8b6c
📒 Files selected for processing (5)
packages/delivery/src/delivery.tspackages/delivery/src/index.tspackages/delivery/src/relaycast.test.tspackages/delivery/src/relaycast.tspackages/delivery/src/types.ts
Review: PR #255 — feat(delivery): add relaycast targetWhat the PR doesAdds a third delivery transport, Correctness assessment
CI / verification note (important)I installed deps and ran the delivery build (
Root cause is test-environment leakage, not a PR defect: this reviewer sandbox exports Note: the working tree at Addressed comments
Advisory Notes
I could not get a clean full-suite run in this sandbox (env-var leak on the test side plus a mid-run filesystem swap of the working tree), and at least one required behavior depends on a human decision about test isolation. Therefore I am not printing READY. |
- Bound the relaycast DM with fetchWithTimeout instead of bare fetch (gemini,
coderabbit) — no indefinite hang if the gateway stalls; degrade to {ok:false}.
- Authenticate /v1/dm with the AGENT token, not the workspace key (codex P1):
WORKFORCE_AGENT_TOKEN > RELAY_AGENT_TOKEN > RELAY_API_KEY fallback. Workspace
key alone gets rejected by the agent-scoped /dm endpoint.
- Unwrap the relaycast `{ ok, data }` envelope before reading the message id
(codex P2) — id lives under data.message.id / data.id; previously parsed
top-level only and returned messageId:''.
- Treat ok:true with no messageId as a failed delivery (return null), matching
slack/telegram missing-receipt handling (coderabbit).
- Keep relaycast out of publish()/non-blocking sends — it's a single blocking
DM with no draft-ref/threading path (coderabbit); skip + debug-log instead.
Tests: +missing-id-is-failure, +publish-skips-relaycast. 8/8 green.
Refs #254
|
pr-reviewer could not complete review for #255 in AgentWorkforce/workforce. |
First piece of #254 (platform-level relay reply). Makes
@agentworkforce/deliveryable to reply over the relay, so agent-to-agent chat works without per-persona code.What
relaycastas a first-class delivery target next toslack/telegram. The existing "reply to origin transport" pattern (hn-monitor, inbox-buddy, joke-bot) now covers relay automatically.SLACK_CHANNEL/TELEGRAM_CHAT); the relaycast reply address is the inbound message's sender, passed viatransports.relaycast = { to, sender? }.resolveDeliveryTargets(ctx)stays slack/telegram-only, so existing callers are unchanged — relaycast only becomes a target when an address is supplied.POST /v1/dmwith the box's injectedRELAY_API_KEY(never throws —{ok:false}+ log on failure).Base URL — easy to change (per the cutover to
cast.agentrelay.com)Single source of truth in
relaycast.ts:Change one constant (or set one env var) to move the gateway. Default is now
cast.agentrelay.com.Files
types.ts—RelaycastRef,RelaycastSender,RelaycastTarget,DeliveryProvider; widenedMessageRef/DeliveryClient.targets/onlyTargets.relaycast.ts(new) —DEFAULT_RELAYCAST_URL,resolveRelaycastUrl,defaultRelaycastSender.delivery.ts— relaycast target wiring +sendRelaycast.relaycast.test.ts(new) — URL precedence + relaycast delivery path. 6/6 green, typecheck clean.Not in this PR (tracked in #254)
ctx.relay.reply()+ surface the inbound sender on the relay event (so callers can populatetoergonomically)inbox_selectorsto['@self'](relay-on by default)@agentworkforce/delivery, consume in the agents repo, drop hn-monitor's slack-only relay fallbackOpen question (flag for #254 acceptance)
The cloud-minted workspace key currently authenticates on
api.relaycast.dev, while this PR defaults tocast.agentrelay.comand cloud infra injectsgateway.relaycast.dev. The env override makes this configurable, but we should converge the box's injected base URL + key onto one gateway before the round-trip works in prod.Refs #254
🤖 Generated with Claude Code